iT邦幫忙

2022 iThome 鐵人賽

DAY 22
0
自我挑戰組

Spring In Action系列 第 22

Functional request handler

  • 分享至 

  • xImage
  •  

在建置SpringMVC時,常常使用annotation的方式來定義功能。不過annotation在開發時很便捷,可是對於不熟悉Spring的開發者,或者是想要針對annotation進行customize時就會很不方便,因為我們無法在貼annotation的地方直接寫客製邏輯,這些邏輯得寫在另一個地方,造成維護與debug的不易。

所以有另一種開發模式,叫做functional,有點Java lambda的味道,可以直接把我們想要的功能寫出來,而不是透過annotation來做代表。

在WebFlux的Spring中,常用以下四種類別:

  • RequestPredicate

定義要處理哪種request

  • RouterFunction

定義接近來的request要做哪些事情

  • ServerRequest

HTTP request, 包含header與body的資訊

  • ServerResponse

HTTP response, 包含header與body的資訊

以下來看個GET的範例:

@Configuration
public class RouterFunctionConfig {
  @Bean
  public RouterFunction<?> helloRouterFunction() {
    return route(GET("/hello"),
        request -> ok().body(just("Hello World!"), String.class));
  }
}

若要處理多個request endpoint,可以如下接著:

@Bean
public RouterFunction<?> helloRouterFunction() {
  return route(GET("/hello"),
      request -> ok().body(just("Hello World!"), String.class))
      .andRoute(GET("/bye"),
      request -> ok().body(just("Bye bye!"), String.class));
}

對比先前建置的OrderController:

import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.POST;
import static org.springframework.web.reactive.function.server.RequestPredicates.queryParam;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
import java.net.URI;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;

@Configuration
public class RouterFunctionConfig {
  @Autowired
  private OrderRepository orderRepo;
  @Bean
  public RouterFunction<?> routerFunction() {
    return route(GET("/api/orders").
              and(queryParam("recent", t->t != null )),
              this::recents)
       .andRoute(POST("/api/orders"), this::postOrder);
  }
  public Mono<ServerResponse> recents(ServerRequest request) {
    return ServerResponse.ok()
        .body(orderRepo.findAll().take(12), Order.class);
  }
  public Mono<ServerResponse> postOrder(ServerRequest request) {
    return request.bodyToMono(Order.class)
        .flatMap(order -> orderRepo.save(order))
        .flatMap(savedOrder -> {
            return ServerResponse
                .created(URI.create(
                    "http://localhost:8081/api/orders/" +
                    savedOrder.getId()))
               .body(savedOrder, Order.class);
        });
  } 
}

再來介紹如何測試WebFlux的Controller。

直接上範例:

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

public class OrderControllerTest {
  @Test
  public void shouldReturnRecentOrders() {
    Order[] orders = {
        testOrder(1L), testOrder(2L),
        testOrder(3L), testOrder(4L),
        testOrder(5L), testOrder(6L),
        testOrder(7L), testOrder(8L),
        testOrder(9L), testOrder(10L),
        testOrder(11L), testOrder(12L),
        testOrder(13L), testOrder(14L),
        testOrder(15L), testOrder(16L)
    };
    Flux<Order> orderFlux = Flux.just(orders);
    OrderRepository orderRepo = Mockito.mock(OrderRepository.class);
    when(orderRepo.findAll()).thenReturn(orderFlux);
    WebTestClient testClient = WebTestClient.bindToController(
        new OrderController(orderRepo)
    ).build();
    testClient.get().uri("/api/orders?recent")
      .exchange()
      .expectStatus().isOk()
      .expectBody()
        .jsonPath("$").isArray()
        .jsonPath("$").isNotEmpty()
        .jsonPath("$[0].id").isEqualTo(orders[0].getId().toString())
        .jsonPath("$[0].name").isEqualTo("Order 1")
        .jsonPath("$[1].id").isEqualTo(orders[1].getId().toString())
        .jsonPath("$[1].name").isEqualTo("Order 2")
        .jsonPath("$[11].id").isEqualTo(orders[11].getId().toString())
        .jsonPath("$[11].name").isEqualTo("Order 12")
        .jsonPath("$[12]").doesNotExist();
  }
  ...
}

jsonPath太醜了,可以用json來比對整串String:

ClassPathResource recentsResource =
    new ClassPathResource("/orders/recent-orders.json");
String recentsJson = StreamUtils.copyToString(
    recentsResource.getInputStream(), Charset.defaultCharset());
testClient.get().uri("/api/orders?recent")
  .accept(MediaType.APPLICATION_JSON)
  .exchange()
  .expectStatus().isOk()
  .expectBody().json(recentsJson);

或者expectBodyList:

testClient.get().uri("/api/orders?recent")
  .accept(MediaType.APPLICATION_JSON)
  .exchange()
  .expectStatus().isOk()
  .expectBodyList(Order.class)
    .contains(Arrays.copyOf(orders, 12));

以下為測試post的範例:

@SuppressWarnings("unchecked")
@Test
public void shouldSaveAOrder() {
  OrderRepository orderRepo = Mockito.mock(OrderRepository.class);
  WebTestClient testClient = WebTestClient.bindToController(
      new OrderController(orderRepo)
  ).build();
  Mono<Order> unsavedOrderMono = Mono.just(testOrder(1L));
  Order savedOrder = testOrder(1L);
  Flux<Order> savedOrderMono = Flux.just(savedOrder);
  when(orderRepo.saveAll(any(Mono.class))).thenReturn(savedOrderMono);
  testClient.post()
      .uri("/api/orders")
      .contentType(MediaType.APPLICATION_JSON)
      .body(unsavedOrderMono, Order.class)
      .exchange()
      .expectStatus().isCreated()
      .expectBody(Order.class)
      .isEqualTo(savedOrder);
}

前面這些測試都是透過mock的實作來實現,不會啟動server,不過其實也可以真的起一個server來做測試。

import java.io.IOException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.reactive.server.WebTestClient;

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT)
public class OrderControllerWebTest {
  @Autowired
  private WebTestClient testClient;
}
@Test
public void shouldReturnRecentOrders() throws IOException {
  testClient.get().uri("/api/orders?recent")
    .accept(MediaType.APPLICATION_JSON).exchange()
    .expectStatus().isOk()
    .expectBody()
        .jsonPath("$").isArray()
        .jsonPath("$.length()").isEqualTo(3)
        .jsonPath("$[?(@.name == 'Conference')]").exists()
        .jsonPath("$[?(@.name == 'Michael')]").exists()
        .jsonPath("$[?(@.name == 'One O one')]").exists();
}

上一篇
Spring WebFlux in controller
下一篇
WebFlux security
系列文
Spring In Action30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言